local Helpers = require("avante.history.helpers")
local Line = require("avante.ui.line")
local Utils = require("avante.utils")
local Highlights = require("avante.highlights")

local M = {}

---@diagnostic disable-next-line: deprecated
local islist = vim.islist or vim.tbl_islist

---Converts text into format suitable for UI
---@param text string
---@param decoration string | nil
---@param filter? fun(text_line: string, idx: integer, len: integer): boolean
---@return avante.ui.Line[]
local function text_to_lines(text, decoration, filter)
  local text_lines = vim.split(text, "\n")
  local lines = {}
  for idx, text_line in ipairs(text_lines) do
    if filter and not filter(text_line, idx, #text_lines) then goto continue end
    if decoration then
      table.insert(lines, Line:new({ { decoration }, { text_line } }))
    else
      table.insert(lines, Line:new({ { text_line } }))
    end
    ::continue::
  end
  return lines
end

---Converts text into format suitable for UI
---@param text string
---@param decoration string | nil
---@param truncate boolean | nil
---@return avante.ui.Line[]
local function text_to_truncated_lines(text, decoration, truncate)
  local text_lines = vim.split(text, "\n")
  local lines = {}
  for _, text_line in ipairs(text_lines) do
    if truncate and #lines > 3 then
      table.insert(
        lines,
        Line:new({
          { decoration },
          {
            string.format("... (Result truncated, remaining %d lines not shown)", #text_lines - #lines + 1),
            Highlights.AVANTE_COMMENT_FG,
          },
        })
      )
      break
    end
    table.insert(lines, Line:new({ { decoration }, { text_line } }))
  end
  return lines
end

---@param lines avante.ui.Line[]
---@param decoration string | nil
---@param truncate boolean | nil
---@return avante.ui.Line[]
local function lines_to_truncated_lines(lines, decoration, truncate)
  local truncated_lines = {}
  for idx, line in ipairs(lines) do
    if truncate and #truncated_lines > 3 then
      table.insert(
        truncated_lines,
        Line:new({
          { decoration },
          {
            string.format("... (Result truncated, remaining %d lines not shown)", #lines - idx + 1),
            Highlights.AVANTE_COMMENT_FG,
          },
        })
      )
      break
    end
    table.insert(truncated_lines, line)
  end
  return truncated_lines
end

---Converts "thinking" item into format suitable for UI
---@param item AvanteLLMMessageContentItem
---@return avante.ui.Line[]
local function thinking_to_lines(item)
  local text = item.thinking or item.data or ""
  local text_lines = vim.split(text, "\n")
  --- trim prefix empty lines
  while #text_lines > 0 and text_lines[1] == "" do
    table.remove(text_lines, 1)
  end
  --- trim suffix empty lines
  while #text_lines > 0 and text_lines[#text_lines] == "" do
    table.remove(text_lines, #text_lines)
  end
  local ui_lines = {}
  table.insert(ui_lines, Line:new({ { Utils.icon("🤔 ") .. "Thought content:" } }))
  table.insert(ui_lines, Line:new({ { "" } }))
  for _, text_line in ipairs(text_lines) do
    table.insert(ui_lines, Line:new({ { "> " .. text_line } }))
  end
  return ui_lines
end

---Converts logs generated by a tool during execution into format suitable for UI
---@param tool_name string
---@param logs string[]
---@return avante.ui.Line[]
function M.tool_logs_to_lines(tool_name, logs)
  local ui_lines = {}
  local num_logs = #logs

  for log_idx = 1, num_logs do
    local log_lines = vim.split(logs[log_idx]:gsub("^%[" .. tool_name .. "%]: ", "", 1), "\n")
    local num_lines = #log_lines

    for line_idx = 1, num_lines do
      local decoration = "│  "
      table.insert(ui_lines, Line:new({ { decoration }, { " " .. log_lines[line_idx] } }))
    end
  end
  return ui_lines
end

local STATE_TO_HL = {
  generating = "AvanteStateSpinnerToolCalling",
  failed = "AvanteStateSpinnerFailed",
  succeeded = "AvanteStateSpinnerSucceeded",
}

function M.get_diff_lines(old_str, new_str, decoration, truncate)
  local lines = {}
  local line_count = 0
  local old_lines = vim.split(old_str, "\n")
  local new_lines = vim.split(new_str, "\n")
  ---@diagnostic disable-next-line: assign-type-mismatch, missing-fields
  local patch = vim.diff(old_str, new_str, { ---@type integer[][]
    algorithm = "histogram",
    result_type = "indices",
    ctxlen = vim.o.scrolloff,
  })
  local prev_start_a = 0
  local truncated_lines = 0
  for _, hunk in ipairs(patch) do
    local start_a, count_a, start_b, count_b = unpack(hunk)
    local no_change_lines = vim.list_slice(old_lines, prev_start_a, start_a - 1)
    if truncate then
      local last_three_no_change_lines = vim.list_slice(no_change_lines, #no_change_lines - 3)
      truncated_lines = truncated_lines + #no_change_lines - #last_three_no_change_lines
      if #no_change_lines > 4 then
        table.insert(lines, Line:new({ { decoration }, { "...", Highlights.AVANTE_COMMENT_FG } }))
      end
      no_change_lines = last_three_no_change_lines
    end
    for idx, line in ipairs(no_change_lines) do
      if truncate and line_count > 10 then
        truncated_lines = truncated_lines + #no_change_lines - idx
        break
      end
      line_count = line_count + 1
      table.insert(lines, Line:new({ { decoration }, { line } }))
    end
    prev_start_a = start_a + count_a
    if count_a > 0 then
      local delete_lines = vim.list_slice(old_lines, start_a, start_a + count_a - 1)
      for idx, line in ipairs(delete_lines) do
        if truncate and line_count > 10 then
          truncated_lines = truncated_lines + #delete_lines - idx
          break
        end
        line_count = line_count + 1
        table.insert(lines, Line:new({ { decoration }, { line, Highlights.TO_BE_DELETED_WITHOUT_STRIKETHROUGH } }))
      end
    end
    if count_b > 0 then
      local create_lines = vim.list_slice(new_lines, start_b, start_b + count_b - 1)
      for idx, line in ipairs(create_lines) do
        if truncate and line_count > 10 then
          truncated_lines = truncated_lines + #create_lines - idx
          break
        end
        line_count = line_count + 1
        table.insert(lines, Line:new({ { decoration }, { line, Highlights.INCOMING } }))
      end
    end
  end
  if prev_start_a < #old_lines then
    -- Append remaining old_lines
    local no_change_lines = vim.list_slice(old_lines, prev_start_a, #old_lines)
    local first_three_no_change_lines = vim.list_slice(no_change_lines, 1, 3)
    for idx, line in ipairs(first_three_no_change_lines) do
      if truncate and line_count > 10 then
        truncated_lines = truncated_lines + #first_three_no_change_lines - idx
        break
      end
      line_count = line_count + 1
      table.insert(lines, Line:new({ { decoration }, { line } }))
    end
  end
  if truncate and truncated_lines > 0 then
    table.insert(
      lines,
      Line:new({
        { decoration },
        {
          string.format("... (Result truncated, remaining %d lines not shown)", truncated_lines),
          Highlights.AVANTE_COMMENT_FG,
        },
      })
    )
  end
  return lines
end

---@param content any
---@param decoration string | nil
---@param truncate boolean | nil
function M.get_content_lines(content, decoration, truncate)
  local lines = {}
  local content_obj = content
  if type(content) == "string" then
    local ok, content_obj_ = pcall(vim.json.decode, content)
    if ok then content_obj = content_obj_ end
  end
  if type(content_obj) == "table" then
    if islist(content_obj) then
      local all_lines = {}
      for _, content_item in ipairs(content_obj) do
        if type(content_item) == "string" then
          local lines_ = text_to_lines(content_item, decoration)
          all_lines = vim.list_extend(all_lines, lines_)
        end
      end
      local lines_ = lines_to_truncated_lines(all_lines, decoration, truncate)
      lines = vim.list_extend(lines, lines_)
    end
    if type(content_obj.content) == "string" then
      local lines_ = text_to_truncated_lines(content_obj.content, decoration, truncate)
      lines = vim.list_extend(lines, lines_)
    end
    if islist(content_obj.content) then
      local all_lines = {}
      for _, content_item in ipairs(content_obj.content) do
        if type(content_item) == "string" then
          local lines_ = text_to_lines(content_item, decoration)
          all_lines = vim.list_extend(all_lines, lines_)
        end
      end
      local lines_ = lines_to_truncated_lines(all_lines, decoration, truncate)
      lines = vim.list_extend(lines, lines_)
    end
    if islist(content_obj.matches) then
      local all_lines = {}
      for _, content_item in ipairs(content_obj.matches) do
        if type(content_item) == "string" then
          local lines_ = text_to_lines(content_item, decoration)
          all_lines = vim.list_extend(all_lines, lines_)
        end
      end
      local lines_ = lines_to_truncated_lines(all_lines, decoration, truncate)
      lines = vim.list_extend(lines, lines_)
    end
  end
  if type(content_obj) == "string" then
    local lines_ = text_to_lines(content_obj, decoration)
    local line_count = 0
    for idx, line in ipairs(lines_) do
      if truncate and line_count > 3 then
        table.insert(
          lines,
          Line:new({
            { decoration },
            {
              string.format("... (Result truncated, remaining %d lines not shown)", #lines_ - idx + 1),
              Highlights.AVANTE_COMMENT_FG,
            },
          })
        )
        break
      end
      line_count = line_count + 1
      table.insert(lines, line)
    end
  end
  if type(content_obj) == "number" then
    table.insert(lines, Line:new({ { decoration }, { tostring(content_obj) } }))
  end
  if islist(content) then
    for _, content_item in ipairs(content) do
      local line_count = 0
      if content_item.type == "content" then
        if content_item.content.type == "text" then
          local lines_ = text_to_lines(content_item.content.text, decoration, function(text_line, idx, len)
            if idx == 1 and text_line:match("^%s*```%s*$") then return false end
            if idx == len and text_line:match("^%s*```%s*$") then return false end
            return true
          end)
          for idx, line in ipairs(lines_) do
            if truncate and line_count > 3 then
              table.insert(
                lines,
                Line:new({
                  { decoration },
                  {
                    string.format("... (Result truncated, remaining %d lines not shown)", #lines_ - idx + 1),
                    Highlights.AVANTE_COMMENT_FG,
                  },
                })
              )
              break
            end
            line_count = line_count + 1
            table.insert(lines, line)
          end
        end
      elseif
        content_item.type == "diff"
        and content_item.oldText ~= nil
        and content_item.newText ~= nil
        and content_item.oldText ~= vim.NIL
        and content_item.newText ~= vim.NIL
      then
        local relative_path = Utils.relative_path(content_item.path)
        table.insert(lines, Line:new({ { decoration }, { "Path: " .. relative_path } }))
        local lines_ = M.get_diff_lines(content_item.oldText, content_item.newText, decoration, truncate)
        lines = vim.list_extend(lines, lines_)
      end
    end
  end
  return lines
end

---@param message avante.HistoryMessage
---@return string tool_name
---@return string | nil error
function M.get_tool_display_name(message)
  local content = message.message.content
  if type(content) ~= "table" then return "", "expected message content to be a table" end

  ---@cast content AvanteLLMMessageContentItem[]

  if not islist(content) then return "", "expected message content to be a list" end

  local item = message.message.content[1]

  local native_tool_name = item.name
  if native_tool_name == "other" and message.acp_tool_call then
    native_tool_name = message.acp_tool_call.title or "Other"
  end
  if message.acp_tool_call and message.acp_tool_call.title then native_tool_name = message.acp_tool_call.title end
  local tool_name = native_tool_name
  if message.displayed_tool_name then
    tool_name = message.displayed_tool_name
  else
    local param
    if item.input and type(item.input) == "table" then
      local path
      if type(item.input.path) == "string" then path = item.input.path end
      if type(item.input.rel_path) == "string" then path = item.input.rel_path end
      if type(item.input.filepath) == "string" then path = item.input.filepath end
      if type(item.input.file_path) == "string" then path = item.input.file_path end
      if type(item.input.query) == "string" then param = item.input.query end
      if type(item.input.pattern) == "string" then param = item.input.pattern end
      if type(item.input.command) == "string" then
        param = item.input.command
        local pieces = vim.split(param, "\n")
        if #pieces > 1 then param = pieces[1] .. "..." end
      end
      if native_tool_name == "execute" and not param then
        if message.acp_tool_call and message.acp_tool_call.title then param = message.acp_tool_call.title end
      end
      if not param and path then
        local relative_path = Utils.relative_path(path)
        param = relative_path
      end
    end
    if not param and message.acp_tool_call then
      if message.acp_tool_call.locations then
        for _, location in ipairs(message.acp_tool_call.locations) do
          if location.path then
            local relative_path = Utils.relative_path(location.path)
            param = relative_path
            break
          end
        end
      end
    end
    if
      not param
      and message.acp_tool_call
      and message.acp_tool_call.rawInput
      and message.acp_tool_call.rawInput.command
    then
      param = message.acp_tool_call.rawInput.command
      pcall(function()
        local project_root = Utils.root.get()
        param = param:gsub(project_root .. "/?", "")
      end)
    end
    if param then tool_name = native_tool_name .. "(" .. vim.inspect(param) .. ")" end
  end

  ---@cast tool_name string

  return tool_name, nil
end

---Converts a tool invocation into format suitable for UI
---@param item AvanteLLMMessageContentItem
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@param expanded boolean | nil
---@return avante.ui.Line[]
local function tool_to_lines(item, message, messages, expanded)
  -- local logs = message.tool_use_logs
  local lines = {}

  local tool_name, error = M.get_tool_display_name(message)
  if error then
    table.insert(lines, Line:new({ { "❌ " }, { error } }))
    return lines
  end

  local rest_input_text_lines = {}

  local result = Helpers.get_tool_result(item.id, messages)
  local state
  if not result then
    state = "generating"
  elseif result.is_error then
    state = "failed"
  else
    state = "succeeded"
  end
  table.insert(
    lines,
    Line:new({
      { "╭─ " },
      { " " .. tool_name .. " ", STATE_TO_HL[state] },
      { " " .. state },
    })
  )
  -- if logs then vim.list_extend(lines, tool_logs_to_lines(item.name, logs)) end
  local decoration = "│   "
  if rest_input_text_lines and #rest_input_text_lines > 0 then
    local lines_ = text_to_lines(table.concat(rest_input_text_lines, "\n"), decoration)
    local line_count = 0
    for idx, line in ipairs(lines_) do
      if not expanded and line_count > 3 then
        table.insert(
          lines,
          Line:new({
            { decoration },
            {
              string.format("... (Input truncated, remaining %d lines not shown)", #lines_ - idx + 1),
              Highlights.AVANTE_COMMENT_FG,
            },
          })
        )
        break
      end
      line_count = line_count + 1
      table.insert(lines, line)
    end
    table.insert(lines, Line:new({ { decoration }, { "" } }))
  end
  local add_diff_lines = false
  if item.input and type(item.input) == "table" then
    if type(item.input.old_str) == "string" and type(item.input.new_str) == "string" then
      local diff_lines = M.get_diff_lines(item.input.old_str, item.input.new_str, decoration, not expanded)
      add_diff_lines = true
      vim.list_extend(lines, diff_lines)
    end
  end
  if
    not add_diff_lines
    and message.acp_tool_call
    and message.acp_tool_call.rawInput
    and message.acp_tool_call.rawInput.oldString
  then
    local diff_lines = M.get_diff_lines(
      message.acp_tool_call.rawInput.oldString,
      message.acp_tool_call.rawInput.newString,
      decoration,
      not expanded
    )
    vim.list_extend(lines, diff_lines)
  end
  if
    message.acp_tool_call
    and message.acp_tool_call.rawOutput
    and message.acp_tool_call.rawOutput.metadata
    and message.acp_tool_call.rawOutput.metadata.preview
  then
    local preview = message.acp_tool_call.rawOutput.metadata.preview
    if preview then
      local content_lines = M.get_content_lines(preview, decoration, not expanded)
      vim.list_extend(lines, content_lines)
    end
  else
    if message.acp_tool_call and message.acp_tool_call.content then
      local content = message.acp_tool_call.content
      if content then
        local content_lines = M.get_content_lines(content, decoration, not expanded)
        vim.list_extend(lines, content_lines)
      end
    else
      if result and result.content then
        local result_content = result.content
        if result_content then
          local content_lines = M.get_content_lines(result_content, decoration, not expanded)
          vim.list_extend(lines, content_lines)
        end
      end
    end
  end
  if #lines <= 1 then
    if state == "generating" then
      table.insert(lines, Line:new({ { decoration }, { "...", Highlights.AVANTE_COMMENT_FG } }))
    else
      table.insert(lines, Line:new({ { decoration }, { "completed" } }))
    end
  end
  --- remove last empty lines
  while #lines > 0 and lines[#lines].sections[2] and lines[#lines].sections[2][1] == "" do
    table.remove(lines, #lines)
  end
  local last_line = lines[#lines]
  last_line.sections[1][1] = "╰─  "
  return lines
end

---Converts a message item into representation suitable for UI
---@param item AvanteLLMMessageContentItem
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@param expanded boolean | nil
---@return avante.ui.Line[]
local function message_content_item_to_lines(item, message, messages, expanded)
  if type(item) == "string" then
    return text_to_lines(item)
  elseif type(item) == "table" then
    if item.type == "thinking" or item.type == "redacted_thinking" then
      return thinking_to_lines(item)
    elseif item.type == "text" then
      return text_to_lines(item.text)
    elseif item.type == "image" then
      return { Line:new({ { "![image](" .. item.source.media_type .. ": " .. item.source.data .. ")" } }) }
    elseif item.type == "tool_use" and item.name then
      local ok, llm_tool = pcall(require, "avante.llm_tools." .. item.name)
      if ok then
        local tool_result_message = Helpers.get_tool_result_message(item.id, messages)
        ---@cast llm_tool AvanteLLMTool
        if llm_tool.on_render then
          return llm_tool.on_render(item.input, {
            logs = message.tool_use_logs,
            state = message.state,
            store = message.tool_use_store,
            result_message = tool_result_message,
          })
        end
      end

      local lines = tool_to_lines(item, message, messages, expanded)
      if message.tool_use_log_lines then lines = vim.list_extend(lines, message.tool_use_log_lines) end
      return lines
    end
  end
  return {}
end

---Converts a message into representation suitable for UI
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@param expanded boolean | nil
---@return avante.ui.Line[]
function M.message_to_lines(message, messages, expanded)
  if message.displayed_content then return text_to_lines(message.displayed_content) end
  local content = message.message.content
  if type(content) == "string" then return text_to_lines(content) end
  if islist(content) then
    local lines = {}
    for _, item in ipairs(content) do
      local item_lines = message_content_item_to_lines(item, message, messages, expanded)
      lines = vim.list_extend(lines, item_lines)
    end
    return lines
  end
  return {}
end

---Converts a message item into text representation
---@param item AvanteLLMMessageContentItem
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@return string
local function message_content_item_to_text(item, message, messages)
  local lines = message_content_item_to_lines(item, message, messages)
  return vim.iter(lines):map(function(line) return tostring(line) end):join("\n")
end

---Converts a message into text representation
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@return string
function M.message_to_text(message, messages)
  local content = message.message.content
  if type(content) == "string" then return content end
  if islist(content) then
    return vim
      .iter(content)
      :map(function(item) return message_content_item_to_text(item, message, messages) end)
      :filter(function(text) return text ~= "" end)
      :join("\n")
  end
  return ""
end

return M
